package com.flow.framework.module.call.rpc.decoder;

import com.flow.framework.common.constant.FrameworkCommonConstant;
import com.flow.framework.common.error.SystemErrorCode;
import com.flow.framework.common.exception.CheckedException;
import com.flow.framework.common.util.verify.VerifyUtil;
import com.flow.framework.module.call.rpc.util.FeignUtil;
import feign.Request;
import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;

import static feign.Util.RETRY_AFTER;
import static feign.Util.checkNotNull;
import static java.util.Locale.US;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * 自定义RPC调用失败的解码器，为响应body和框架Response的字符串的反序列化
 *
 * @author luoguopiao
 * @version 0.0.1
 * @date 2022/2/19
 */
@Slf4j
public class CustomizationRpcFailedDecoder implements ErrorDecoder {

    private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder();

    /**
     * 响应记录时的最大body长度
     */
    private static final int MAX_BODY_BYTES_LENGTH = 16384;

    /**
     * @inheritDoc
     */
    @Override
    public Exception decode(String methodKey, Response response) {
        Request request = response.request();
        Map<String, Collection<String>> headers = response.headers();
        String url = request.url();
        com.flow.framework.core.response.Response<String> frameworkResponse = null;
        if (RpcHelper.isInnerRequest(response)) {
            frameworkResponse = RpcHelper.getFrameworkResponse(headers, url);
        }
        byte[] bytes = FeignUtil.getResponseByteBody(response, MAX_BODY_BYTES_LENGTH);
        String stringBody = new String(bytes, FeignUtil.getResponseCharset(headers));
        if (bytes.length == MAX_BODY_BYTES_LENGTH) {
            stringBody = stringBody + "..., has more content";
        }
        log.error("remote call error. url: {}, http code: {}, response body: {}", url, response.status(), stringBody);

        CheckedException exception;
        if (null != frameworkResponse) {
            String code = frameworkResponse.getCode();
            String msg = frameworkResponse.getMsg();
            String system = frameworkResponse.getSystem();
            List<String> params = frameworkResponse.getParams();

            String srcCode = frameworkResponse.getSrcCode();
            if (!VerifyUtil.isEmpty(srcCode)) {
                exception = new CheckedException(SystemErrorCode.REMOTE_PROCEDURE_CALL_ERROR, msg, null,
                        frameworkResponse.getSrcSystem(), srcCode, frameworkResponse.getSrcMsg(), frameworkResponse.getSrcParams());
            } else {
                exception = new CheckedException(SystemErrorCode.REMOTE_PROCEDURE_CALL_ERROR, msg, null, system, code, msg, params);
            }
        } else {
            exception = new CheckedException(SystemErrorCode.REMOTE_PROCEDURE_CALL_ERROR);
        }

        Date retryAfter = retryAfterDecoder.apply(firstOrNull(headers, RETRY_AFTER));
        if (retryAfter != null) {
            return new RetryableException(
                    response.status(),
                    stringBody,
                    request.httpMethod(),
                    exception,
                    retryAfter,
                    request);
        }
        return exception;
    }

    private <T> T firstOrNull(Map<String, Collection<T>> map, String key) {
        if (map.containsKey(key) && !map.get(key).isEmpty()) {
            return map.get(key).iterator().next();
        }
        return null;
    }

    /**
     * Decodes a {@link feign.Util#RETRY_AFTER} header into an absolute date, if possible. <br>
     * See <a href="https://tools.ietf.org/html/rfc2616#section-14.37">Retry-After format</a>
     */
    static class RetryAfterDecoder {

        static final DateFormat RFC822_FORMAT =
                new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", US);
        private final DateFormat rfc822Format;

        RetryAfterDecoder() {
            this(RFC822_FORMAT);
        }

        RetryAfterDecoder(DateFormat rfc822Format) {
            this.rfc822Format = checkNotNull(rfc822Format, "rfc822Format");
        }

        protected long currentTimeMillis() {
            return System.currentTimeMillis();
        }

        /**
         * returns a date that corresponds to the first time a request can be retried.
         *
         * @param retryAfter String in
         *                   <a href="https://tools.ietf.org/html/rfc2616#section-14.37" >Retry-After format</a>
         */
        public Date apply(String retryAfter) {
            if (retryAfter == null) {
                return null;
            }
            if (retryAfter.matches("^[0-9]+\\.?0*$")) {
                retryAfter = retryAfter.replaceAll("\\.0*$", FrameworkCommonConstant.EMPTY_STRING);
                long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter));
                return new Date(currentTimeMillis() + deltaMillis);
            }
            synchronized (rfc822Format) {
                try {
                    return rfc822Format.parse(retryAfter);
                } catch (ParseException ignored) {
                    return null;
                }
            }
        }
    }
}
